package org.fjsei.yewu.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import md.system.UserRepository;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Slf4j
@Component
public class JwtTokenProvider implements InitializingBean {

    private static final String AUTHORITIES_KEY = "auth";
    //密钥还不能太短了！报错not secure enough for any JWT HMAC-SHA algorithm. >= 256 bits
    private final String secret;
    private final long accessTokenValidityInMilliseconds;
    private final long refreshTokenValidityInMilliseconds;
    //转换后的密码
    private Key key;
    //用户数据库
    @Autowired
    protected UserRepository userRepository;
    //实际装入是 JwtUserDetailsService:
    private final UserDetailsService userDetailsService;
    @Value("${server.ssl.enabled:false}")
    private boolean  isSSLenabled;
    @Value("${sei.server.URI:}")
    private String serverURI;

    public JwtTokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds,
            @Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValidityInSeconds,
            UserDetailsService userDetailsService){
        this.secret = secret;
        this.accessTokenValidityInMilliseconds = accessTokenValidityInSeconds * 1000;
        this.refreshTokenValidityInMilliseconds = refreshTokenValidityInSeconds * 1000;
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

/** 需要在authenticate()接口进行单点登录适应机制改造的，而不是这里。
 * 假如SSO单点登录 auth0 authing登录云服务 OAuth2: 也是需要本地签发自己的Token已经本地角色授权，本地服务只是缺少密码验证和密码存储环节。
 * camunda-Identity模块：而且要配套 Keycloak 实例运行;  没云容器无法本地方式安装；	  https://docs.camunda.io/docs/self-managed/identity/getting-started/
 * spring-boot-keycloak-starter  集成的单点登录+授权。
 * OpenID Connect/OIDC使用OAuth2.0;  Keycloak用于REST;配合Spring MVC的@RequestMapping;
 * 认证（Authentication）即确认该用户的身份是他所声明的那个人; 授权（Authorization）即根据用户身份授予他访问特定资源的权限;
 * CAS是一种仅用于Authentication的服务，它和OAuth/OIDC协议不一样，并不能作为一种Authorization的协议。
 * OAuth 2.0协议，它是一种Authorization协议并不是一种Authentication协议;
 * SAML协议 不支持移动端APP非浏览器的SSO;  旧的+UI管理器不适合前后端分离架构的。
 * Keycloak基于OAuth 2.1、Open ID Connect、JSON Web Token（JWT）和SAML 2.0规范，为浏览器应用和RESTful Web Service提供SSO和IDM集成;
 * Keycloak前端采用了AngularJS技术; ID plvider
 * pac4j引擎;  cas 认证中心;  PKCE with OpenID Connect手机端java APP;
 * OpenID Connect; 原理: https://blog.csdn.net/qq_24550639/article/details/111089296
 * 部署 Keycloak 服务器,独立运行的; Keycloak OIDC Provider ;
 * JWT伪造   https://blog.csdn.net/qq_45557476/article/details/123171281
* */
    public TokenDto createToken(Authentication authentication) {
//        String authorities = authentication.getAuthorities().stream()
//                .map(GrantedAuthority::getAuthority)
//                .collect(Collectors.joining(","));

        Date now = new Date();
        /* 解析明文的结果类似： JWT只能防止被篡改，只能由发送端来验证正确性。
        {
            "sub": "herzhang",
            "auth": "ROLE_Master,ROLE_JyUser"       一个人可能很多个的用户角色的， #Token太长？删除掉算了。
            "exp": 1665736428
        }  */

        //没有setIssuedAt "iat" 修改密码后旧的token还可以继续生效登录，没法主动挂失token。
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setIssuedAt(now)
                //.claim(AUTHORITIES_KEY, authorities)      .signWith(key, SignatureAlgorithm.HS512)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(new Date(now.getTime() +accessTokenValidityInMilliseconds))
                .compact();

//        String refreshToken = Jwts.builder()
//                .setSubject(authentication.getName())
//                //.claim(AUTHORITIES_KEY, authorities)
//                .signWith(key, SignatureAlgorithm.HS512)
//                .setExpiration(new Date(now.getTime() +refreshTokenValidityInMilliseconds))
//                .compact();
        //还有第三种的 : access_token，id_token 和 refresh_token。
        return TokenDto.builder().accessToken(accessToken).build();
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        Object auths= claims.get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities =null;
        if(null!=auths) {
            authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        } else {
            String username= claims.getSubject();
            md.system.User  user=userRepository.findByUsername(username);
            if(null!=user && null!=user.heHasRoles()){
                authorities =user.heHasRoles().stream()
                        .map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
                        .collect(Collectors.toList());
            }
        }
        //权限认证应该以内存中最后的authentication{..}的为准。数据库读取是恢复步骤。
        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    public Boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            //throw new UserAccessDenyException("无效token");
            log.info("无效token");
            return Boolean.FALSE;
            //throw new CommonGraphQLException("无效token", token);
        } catch (ExpiredJwtException | IllegalArgumentException | UnsupportedJwtException e) {
            return Boolean.FALSE;
        }
        //没有签名的 SignatureAlgorithm.NONE   "alg": "none"免漏洞  #关键和解析流程的预期保持一致。
        return Boolean.TRUE;
    }

    public Boolean validateRefreshToken(String refreshToken){
        try{
            Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken);
            if(!claims.getBody().getExpiration().before(new Date())){
            log.info("正常的");
            }
        }
        catch (Exception e){
            log.info("超期");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    public TokenDto reCreateToken(Authentication authentication) {

//        String authorities = authentication.getAuthorities().stream()
//                .map(GrantedAuthority::getAuthority)
//                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.accessTokenValidityInMilliseconds);

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                //.claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();

        log.info(accessToken);

        return TokenDto.builder().accessToken(accessToken).build();
    }
    //根据token为其续命,更新对应的spring security; 每一次的请求包都要首先恢复身份证明的。
    protected void continuedTokenLifeAuthentication(String token, HttpServletRequest request, HttpServletResponse response)
    {
        if(!StringUtils.hasText(token))      return;     //没有token证书，允许过，但受到具体接口的内部安全措施控制。
//        if (StringUtils.hasText(authToken) && jwtTokenProvider.validateToken(authToken)) {
//            Authentication authentication = getAuthentication(authToken);
//            SecurityContextHolder.getContext().setAuthentication(authentication);
//        }

        String usernameIdc = null;
        Claims claims =null;
        try {
            claims =Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("无效token");
            return ;
        } catch (ExpiredJwtException | IllegalArgumentException | UnsupportedJwtException e) {
            log.info("过期token");
            return ;
        }
        //绕了个大弯 (Claims::getSubject).apply(claims);
        try {
            //username = (Claims::getSubject).apply(claims);   为何手机电脑同时登录，token时间容易失效。
            usernameIdc = claims.getSubject();
        } catch (IllegalArgumentException | ExpiredJwtException e) {
            log.warn("nValid Token", e);
        }
        if(null ==usernameIdc)	    return;

        //用JwtUserDetailsService从数据库找的，　UserDetails这里就是JwtUser了;
        UserDetails userDetails = userDetailsService.loadUserByUsername(usernameIdc);
        //登录已经过期了，应当抛出异常，否则返回都是null，不知道到底是何种情况内还是应用层没有数据。
        if(!validateClaims(claims, userDetails))         return;
        //用户已经被屏蔽了。
        if(!userDetails.isEnabled())        return;

        //给浏览器cookie.和token内部声称的时间不同，浏览器1.5个小时后就过期不给送了。
        if(needTokenToRefresh(claims))
            timeArrivedRegenerateToken(userDetails,request,response);

//        if(SecurityContextHolder.getContext().getAuthentication() == null) {
            //因为Session .STATELESS每一次请求包都需要再次设置身份信息，实际上这里每次请求包都要过。
            //credentials 证明书? 公钥的Key吗;
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            // logger.info("authorizated user '{}', Reset security context", username);
            //这里应该有ROLE_:实际就是userId保留到Context
            SecurityContextHolder.getContext().setAuthentication(authentication);
//        }
//        else {
//            log.info("稀奇!STATELESS竟然");
//        }
    }

    //验证合法性的token; 而每一次登录都会重新生成token的。
    private Boolean validateClaims(Claims claims, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        String username = claims.getSubject();      //绕了个大弯 (Claims::getSubject).apply(claims);
        Date created = claims.getIssuedAt();   //"iat": getIssuedAtDateFromToken(token);
        //final Date expiration = getExpirationDateFromToken(token);
        boolean isTokenExpired;
        final Date expiration = claims.getExpiration();
        Date now = new Date();
        isTokenExpired = expiration.before(now);         //在now以前的
        //密码修改了，lastPasswordReset；
        //username已改成userID了 代替做标识。
        return (
                username.equals( user.getUsername() )
                        && !isTokenExpired
                        && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }
    //密码修改 ？ 没有配置上发行时间没法做到!。 claims.getExpiration();
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }
    //计算更新时机, 客户浏览没动静30分钟；
    private Boolean needTokenToRefresh(Claims claims) {
        final Date expiration = claims.getExpiration();
        Date now = new Date();
        Date  comp= new Date( expiration.getTime()-1000*60*30 );        //milliseconds since January 1, 1970
        return  comp.before(now);
    }
    /** 时间快到了，重新发放证书
    将jwt放到Cookie中反给用户并且使用HttpOnly属性来防止Cookie被JavaScript读取,并且设置一个过期时间来保证安全;
    */
    private void timeArrivedRegenerateToken(UserDetails userDetails, HttpServletRequest request, HttpServletResponse response) {
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));      //?context 关联UserDetails
        SecurityContextHolder.getContext().setAuthentication(authentication);
        TokenDto jwt = createToken(authentication);
        //浏览器自动遵守标准：超时的cookie就不会该送过来了。 那万一不守规矩？两手准备。
        Cookie cookie =new Cookie("token", jwt.getAccessToken());
        // cookie.setDomain(cookieDomain);
        cookie.setHttpOnly(true);
        cookie.setMaxAge((int) (accessTokenValidityInMilliseconds/1000));      //这个时间和token内部声称的时间不同，这给浏览器用的 = 1.5个小时。
        cookie.setPath("/");
        cookie.setSecure(isSSLenabled);
        response.addCookie(cookie);
        log.info("authorizated user '{}', timeArrivedRegenerateToken", userDetails.getUsername());
    }

    public String getServerURI() {
        return serverURI;
    }
}


/* SpEL表达式   https://www.zhyea.com/2019/11/27/springboot-base-04-use-spel.html
@Value("#{systemProperties['user.language']}")
    private String message;
&&，||，!，and，or，not，between，instanceof  算术  <，>，<=，>=，==，!=，lt，gt，le，ge，eq，ne
@GetMapping("/downloadFile/{fileName:.+}")
SpEL怎样从List、Map集合中取值; @Value("#{numberBean.no == 999 and numberBean.no < 900}")
 @PreAuthorize("hasRole('ROLE_'.concat(this.class.simpleName))")
 @PreAuthorize("hasAnyRole('Admin','User')")
接口授权@PreAuthorize拦截之后效果：
    AccessDeniedException: 不允许访问;
JWT Token 解析工具  ;   https://tooltt.com/jwt-decode/
@Value 的注意事项 （以下问题都会造成，无法注入的问题）
    1.不能作用于静态变量（static）     2.不能作用于常量（final）
    3.不能在非注册的类中使用：需要被注册在spring上下文中，如用@Service,@RestController,@Component等；
    4.使用这个类时，只能通过依赖注入的方式，用new Xx(）的方式是不会自动注入这些配置的。
// 限制只能查询 id 小于 10 的用户
@PreAuthorize("#id < 10")
User findById(int id);
// 只能查询自己的信息
 @PreAuthorize("principal.username.equals(#username)")
User find(String username);
// 限制只能新增用户名称为abc的用户
@PreAuthorize("#user.name.equals('abc')")
void add(User user)
// 查询到用户信息后，再验证用户名是否和登录用户名一致
@PostAuthorize("returnObject.name == authentication.name")
public User getUser(String name){
// 验证返回的数是否是偶数
@PostAuthorize("returnObject % 2 == 0")
public Integer test(){
* */
